import sys
import os
import pyqtgraph as pg
import numpy as np
from PyQt5.QtGui import QDoubleValidator
from PyQt5.QtWidgets import QMainWindow, QLabel, QGridLayout, QApplication, QMessageBox
from tools.UI_MainForm import Ui_MainWindow
from tools.WorkingThread import ThreadFactory
from param.Parameter import Parameter
from param.ParamMain import ParamMain
from InfoForm import InfoForm
from CurveOverForm import CurveOverForm
from MeltCharacterForm import MeltCharacterForm
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from pymodbus.payload import BinaryPayloadBuilder
from pymodbus.constants import Endian


class MainForm(QMainWindow, Ui_MainWindow):

    def __init__(self):
        super(MainForm, self).__init__()
        self.setupUi(self)

        # 数据容器初始化，在startTest中调用了
        # self.dataInit()

        # 初始化 ip 和 port
        self.ipPortInit()
        # 初始化各参数地址
        self.addressInit()
        # 各功能状态初始化
        self.stateInit()
        # 初始按钮的功能
        self.btnInit()
        # 初始化相关的文本框
        self.lineEditInit()
        # 初始化各线程
        self.threadInit()
        # 画板初始化
        self.canvasInit()
        # 初始化新建项目的功能
        self.newProjectInit()
        # 后处理窗体初始化
        self.backProcessingInit()
        # 状态栏初始化
        self.statusBarInit()

    def statusBarInit(self):
        self.label_status = QLabel(self)
        self.label_status.setText("通信状态:")
        self.label_status2 = QLabel(self)
        self.label_status2.setText("未连接")

        self.label_project = QLabel(self)
        self.label_project.setText("当前测试项目：")
        self.label_projectName = QLabel(self)
        self.label_projectName.setText("请先新建一个项目！")


        self.sb = self.statusBar()

        self.sb.addWidget(self.label_status)
        self.sb.addWidget(self.label_status2)
        self.sb.addWidget(self.label_project)
        self.sb.addWidget(self.label_projectName)

    def dataInit(self):
        """初始化各个数据容器"""
        # 存放时间轴的数据
        self.timeData = []
        # 存放扭矩的数据
        self.torqueData = []
        # 存放熔体温度的数据
        self.meltTemperData = []
        # 存放温度的数据（应该不需要，先定义一个）
        self.temperData = []
        # 存放速度的数据（应该不需要）
        self.speedData = []

    def stateInit(self):
        """各功能状态初始化"""
        # 是否正在加热
        self.isHeating = False
        # 是否转子正在转动
        self.isTuring = False
        # 是否正在测试
        self.isTesting = False
        # 是否已经新建一个项目？默认为False
        self.isNewProject = False
        # 是否连接上PLC
        self.isConnect = False

    def addressInit(self):
        """通过Parameter从json获取各参数的地址，此后，更改地址参数只需要在json中更改"""
        # self.addrTemperFrontActual = self.parameter.getTemperFrontActual()
        # self.addrTemperMidActual =self.parameter.getTemperMidActual()
        # self.addrTemperBackActual =self.parameter.getTemperBackActual()
        # self.addrSpeedActual =self.parameter.getSpeedActual()
        # self.addrTorque =self.parameter.getTorque()
        # self.addrMeltTemper =self.parameter.getMeltTemper()


        self.addrHeatingOn = self.parameter.getHeatingOn()
        self.addrRotorOn = self.parameter.getRotorOn()

        self.addrTemperFrontSet = self.parameter.getTemperFrontSet()
        self.addrTemperMidSet = self.parameter.getTemperMidSet()
        self.addrTemperBackSet = self.parameter.getTemperBackSet()
        self.addrSpeedSet = self.parameter.getSpeedSet()

        self.MAX_TEMPER = self.parameter.MAX_TEMPER()
        self.MAX_SPEED = self.parameter.MAX_SPEED()

    def btnInit(self):
        """初始按钮的功能"""
        # 连接PLC的槽函数
        # self.btnConnect.clicked.connect(self.connectPLC)
        self.btnConnect.clicked.connect(self.connectModubs)
        # 连接断开PLC的槽函数
        self.btnDisconnect.clicked.connect(self.disconnectModubs)
        # 连接开始加热的槽函数
        self.btnStartHeating.clicked.connect(self.startHeating)
        # 连接停止加热的槽函数
        self.btnStopHeating.clicked.connect(self.stopHeating)
        # 连接开始测试的槽函数
        self.btnStartTest.clicked.connect(self.startTest)
        # 连接停止测试的槽函数
        self.btnStopTest.clicked.connect(self.stopTest)
        # 连接启动转子的槽函数
        self.btnTurnOn.clicked.connect(self.turnOn)
        # 连接停止转子的槽函数
        self.btnTurnOff.clicked.connect(self.turnOff)

        # 初始化界面时将启动测试的按钮失能，直至用户创建了项目信息
        self.btnStartTest.setEnabled(False)

    def lineEditInit(self):
        """绑定文本框的槽函数"""

        # 温度文本框验证器，设置只接受浮点数，范围为（0，350），并且只能输入一位有效数字
        temperValiator = QDoubleValidator()
        temperValiator.setRange(0, 350)
        temperValiator.setNotation(QDoubleValidator.StandardNotation)
        temperValiator.setDecimals(1)

        self.lineEdit_FrontSet.setValidator(temperValiator)
        self.lineEdit_MidSet.setValidator(temperValiator)
        self.lineEdit_BackSet.setValidator(temperValiator)
        self.lineEdit_FrontActual.setValidator(temperValiator)
        self.lineEdit_MidActual.setValidator(temperValiator)
        self.lineEdit_BackActual.setValidator(temperValiator)

        # 速度文本框验证器，设置只接受浮点数，范围为（0，250），并且只能输入一位有效数字
        speedValiator = QDoubleValidator()
        speedValiator.setRange(0, 250)
        speedValiator.setNotation(QDoubleValidator.StandardNotation)
        speedValiator.setDecimals(1)

        self.lineEdit_SpeedSet.setValidator(speedValiator)
        # self.lineEdit_SpeedActual.setValidator(speedValiator)

        self.lineEdit_FrontSet.textChanged.connect(lambda: self.updateSet(0))
        self.lineEdit_MidSet.textChanged.connect(lambda: self.updateSet(1))
        self.lineEdit_BackSet.textChanged.connect(lambda: self.updateSet(2))
        self.lineEdit_SpeedSet.textChanged.connect(lambda: self.updateSet(3))

    def backProcessingInit(self):
        self.curveOverForm = CurveOverForm()
        self.action_curve.triggered.connect(lambda: self.curveOverForm.show())
        self.meltCharacterForm = MeltCharacterForm()
        self.action_MeltForm.triggered.connect(lambda: self.meltCharacterForm.show())

    def connectModubs(self):
        """使用pymodbus建立一个modbus客户端，连接PLC"""
        if not self.ip:
            print(self.ip)
            QMessageBox.critical(self, '警告', 'ip不能为空！请重新输入。')
            self.lineEdit_IP.setFocus()
        elif not self.port:
            QMessageBox.critical(self, '警告', '端口不能为空！请重新输入。')
            self.lineEdit_Port.setFocus()
        else:
            self.modbusClient = ModbusClient(self.ip, int(self.port))
            # 连接成功会返回True，失败会返回False
            if self.modbusClient.connect():
                print("连接成功！")
                self.label_status2.setText("已连接")
                self.isConnect = True
                # 为每个子线程注入modbus客户端
                self.heatingThread.setModbusClient(self.modbusClient)
                self.speedThread.setModbusClient(self.modbusClient)
                self.torqueThread.setModbusClient(self.modbusClient)

                self.startDetect()

                # 使一些控件使能和失能
                self.btnDisconnect.setEnabled(True)
                self.btnConnect.setEnabled(False)
                self.lineEdit_IP.setEnabled(False)
                self.lineEdit_Port.setEnabled(False)
            else:
                QMessageBox.information(self, "超时", "连接失败！请检测Ip地址或端口号是否正确！")

    def disconnectModubs(self):
        """关闭modbus客户端，断开和PLC的连接"""
        if self.isConnect:
            self.isConnect = False
            # 关闭正在进行的测试
            self.stopDetect()
            self.stopTest()
            self.stopHeating()
            self.turnOff()

            # 设置各个线程的modbus客户端为None
            self.heatingThread.setModbusClient(None)
            self.speedThread.setModbusClient(None)
            self.torqueThread.setModbusClient(None)

            # 让相关的控件失能和使能
            self.btnDisconnect.setEnabled(False)
            self.btnConnect.setEnabled(True)
            self.lineEdit_IP.setEnabled(True)
            self.lineEdit_Port.setEnabled(True)

            self.label_status2.setText("未连接")

            # 关闭modbus客户端
            # self.modbusClient.close()

    def canvasInit(self):
        """画板初始化"""

        # 画板控件
        self.canvas = pg.GraphicsLayoutWidget()
        self.subplot = self.canvas.addPlot()
        self.canvas.addItem(self.subplot)
        # 设置横坐标纵坐标的标签
        self.subplot.setLabel(axis='left', text=u'扭矩 / N·m')
        self.subplot.setLabel(axis='bottom', text=u'时间 / s')
        # 实例一条线对象，实时跟新这条线的数据即可
        self.line = self.subplot.plot(pen='r')
        # 背景设置为白色（默认为黑色）
        self.canvas.setBackground("w")
        # 将画板添加到groupbox中
        self.graph_layout = QGridLayout()
        self.graph_layout.addWidget(self.canvas)
        self.groupBox_graph.setLayout(self.graph_layout)

    def threadInit(self):
        """初始化各线程，并将线程中的信号连接到控件刷新的槽函数上"""
        # 实例化线程工厂
        self.threadFactory = ThreadFactory()

        # 从线程工厂中获取监控温度的线程
        self.heatingThread = self.threadFactory.getHeatingThread()
        # 更新控件
        self.heatingThread.heating_on.connect(self.temperChange)

        # 从线程工厂中获取监控转子速度的线程
        self.speedThread = self.threadFactory.getSpeedThread()
        self.speedThread.torque_on.connect(self.speedChange)

        # 从线程工厂中获取监控转矩和熔体温度的线程
        self.torqueThread = self.threadFactory.getTorqueThread()
        self.torqueThread.send_param.connect(self.paramChange)

    def newProjectInit(self):
        """
        初始化新建项目的功能
        """

        # 实例化填报项目信息的窗口对象
        self.infoForm = InfoForm()
        # 连接信号，打开填报项目信息的窗口
        self.action_new.triggered.connect(lambda: self.infoForm.show())
        self.action_new.triggered.connect(self.infoForm.updateFilename)
        # 将填完信息的信号与保存项目信息的槽函数连接
        self.infoForm.finish_signal.connect(self.setProjectInfo)

    def setProjectInfo(self, dirname, filename):
        """设置测试项目保存的路径信息"""
        self.isNewProject = True
        self.btnStartTest.setEnabled(True)
        self.path = dirname + filename
        print(self.path)
        if not os.path.exists(self.path):
            os.mkdir(self.path)

        self.label_projectName.setText(filename)

    def ipPortInit(self):
        """初始化 ip 和 port"""
        path = os.getcwd() + '/param/Parameter.json'
        # 获取参数对象
        self.parameter = Parameter(path)
        # 初始化主窗口的ip和端口文本框
        self.ipPortLineEditInit()
        # 实例化一个设置ip和端口号的窗口对象
        self.paramMain = ParamMain()
        # 接收信号后更新ip和端口号
        self.paramMain.updated_ip_port.connect(self.ipPortLineEditInit)
        # 打开设置ip和端口号的窗口
        self.action_set.triggered.connect(lambda: self.paramMain.show())
        # 检测Ip和端口号是否改变
        self.lineEdit_IP.textChanged.connect(self.updateIpPort)
        self.lineEdit_Port.textChanged.connect(self.updateIpPort)

    def ipPortLineEditInit(self):
        """初始化主窗口的ip和端口号文本框"""
        self.lineEdit_IP.setText(self.parameter.getIP())
        self.lineEdit_Port.setText(self.parameter.getPort())
        self.updateIpPort()

    def updateIpPort(self):
        """更新ip和端口号端口号"""
        self.ip = self.lineEdit_IP.text()
        self.port = self.lineEdit_Port.text()

    def startHeating(self):
        """加热功能"""
        if not self.heatingThread.modbusClient:
            QMessageBox.information(self, "提醒", "还没连接PLC!")
        else:
            # 标记正在加热
            self.isHeating = True
            # 使能和失能相关的控件
            self.btnStartHeating.setEnabled(False)
            self.btnStopHeating.setEnabled(True)
            # TODO
            # 发送一个指令控制下位机开始加热
            self.modbusClient.write_coil(self.addrHeatingOn, 1, unit=1)
            # 同时将温度的设置值传递给PLC
            self.updateSet(0)
            self.updateSet(1)
            self.updateSet(2)

    def stopHeating(self):
        """停止加热"""
        # TODO
        # 发送一个指令控制下位机停止加热
        self.modbusClient.write_coil(self.addrHeatingOn, 0, unit=1)
        # 标记正在加热
        self.isHeating = False

        # 使能和失能相关的控件
        self.btnStartHeating.setEnabled(True)
        self.btnStopHeating.setEnabled(False)

    def temperChange(self, temper1, temper2, temper3):
        """温度变化槽函数"""

        self.lineEdit_FrontActual.setText(temper1)
        self.lineEdit_MidActual.setText(temper2)
        self.lineEdit_BackActual.setText(temper3)

    def startTest(self):
        """开始测试"""
        if not self.isNewProject:
            QMessageBox.information(self, "提醒", "请先创建一个项目!")
            self.label_projectName.setText("请先新建一个项目！")
            return
        elif not self.torqueThread.modbusClient:
            QMessageBox.information(self, "提醒", "还没连接PLC！")
        else:

            # 首先初始化数据容器
            self.dataInit()

            self.btnStopTest.setEnabled(True)
            self.btnStartTest.setEnabled(False)
            self.isTesting = True

            # 开启读取熔体温度和扭矩的线程
            self.testTime = 0

    def stopTest(self):
        """停止测试"""

        if self.isTesting:

            self.isTesting = False
            self.isNewProject = False
            self.btnStopTest.setEnabled(False)
            self.btnStartTest.setEnabled(True)

            # 保存数据
            np.savetxt(self.path + '/Torque.csv', [self.torqueData, self.timeData], delimiter=',')
            np.savetxt(self.path + '/Melt Temper.csv', [self.meltTemperData, self.timeData], delimiter=',')

    def paramChange(self, torque, meltTemper, interval):
        """
        扭矩和熔体温度变化槽函数，同时绘制曲线
        torque:str，扭矩数据
        meltTemper:str, 熔体温度数据
        interval:int, 采样时间间隔
        """

        # 更新文本框
        self.lineEdit_MeltTemper.setText(meltTemper)
        self.lineEdit_Torque.setText(torque)

        # 只有在测试时，才把读取到的数据添加到容器中，并绘制曲线
        if self.isTesting:
            # 更新测试时间
            self.testTime += interval / 1000
            # 将数据添加到各自的容器中
            self.torqueData.append(float(torque))
            self.meltTemperData.append(float(meltTemper))
            self.timeData.append(self.testTime)
            # 更新曲线
            self.line.setData(self.timeData, self.torqueData)
            self.lineEdit_time.setText(self.calTime())

    def calTime(self):
        """将秒数转换成时间格式"""
        m, s = divmod(int(self.testTime), 60)
        h, m = divmod(m, 60)
        return "%02d:%02d:%02d" % (h, m, s)

    def turnOn(self):
        """转子启动"""
        if not self.speedThread.modbusClient:
            QMessageBox.information(self, "提醒", "还没连接PLC！")
        else:
            self.isTuring = True
            self.btnTurnOn.setEnabled(False)
            self.btnTurnOff.setEnabled(True)

            # TODO
            # 发送一个指令启动转子旋转
            self.modbusClient.write_coil(self.addrRotorOn, 1, unit=1)
            # 同时将转子的设置值传递给PLC
            self.updateSet(3)

    def turnOff(self):
        """转子停止"""

        self.isTuring = False
        self.btnTurnOn.setEnabled(True)
        self.btnTurnOff.setEnabled(False)

        # TODO
        # 发送一个指令停止转子旋转
        self.modbusClient.write_coil(self.addrRotorOn, 0, unit=1)

    def speedChange(self, speed):
        """速度变化槽函数"""
        self.lineEdit_SpeedActual.setText(speed)

    def startDetect(self):
        # 开启读取扭矩和熔体温度的线程
        self.torqueThread.start()
        # 启动温度线程监控温度
        self.heatingThread.start()
        # 开始速度监控的线程，实时读取转子的速度
        self.speedThread.start()

    def stopDetect(self):
        # 关闭读取扭矩和熔体温度的线程
        self.torqueThread.quit()
        # 关闭读取温度的线程
        self.heatingThread.quit()
        # 中断读取转子速度的线程
        self.speedThread.quit()

    def updateSet(self, idx: int):
        """
        更新温度、速度的设置值
        idx: int类型，0表示前板温度，1表示中板温度，2表示后板温度，3表示转子速度
        """
        if self.isConnect:
            # 更改前板温度设置
            if idx == 0:
                try:
                    value = float(self.lineEdit_FrontSet.text())
                except Exception as e:
                    value = 0.0

                # 如果设置的温度超过最大值，设置为温度最大值
                if value > self.MAX_TEMPER:
                    value = self.MAX_TEMPER
                    self.lineEdit_FrontSet.setText(str(value))
                registers = self.convert_float_to_registers(value)
                self.modbusClient.write_registers(self.addrTemperFrontSet, registers, unit=1)
            # 更改中板温度设置
            elif idx == 1:
                try:
                    value = float(self.lineEdit_MidSet.text())
                except Exception as e:
                    value = 0.0

                # 如果设置的温度超过最大值，设置为温度最大值
                if value > self.MAX_TEMPER:
                    value = self.MAX_TEMPER
                    self.lineEdit_MidSet.setText(str(value))

                registers = self.convert_float_to_registers(value)
                self.modbusClient.write_registers(self.addrTemperMidSet, registers, unit=1)
            # 更改后板温度设置
            elif idx == 2:
                try:
                    value = float(self.lineEdit_BackSet.text())
                except Exception as e:
                    value = 0.0

                # 如果设置的温度超过最大值，设置为温度最大值
                if value > self.MAX_TEMPER:
                    value = self.MAX_TEMPER
                    self.lineEdit_BackSet.setText(str(value))
                registers = self.convert_float_to_registers(value)
                self.modbusClient.write_registers(self.addrTemperBackSet, registers, unit=1)
            # 更改转子速度设置
            elif idx == 3:
                try:
                    value = float(self.lineEdit_SpeedSet.text())
                except Exception as e:
                    value = 0.0

                # 如果设置的温度超过最大值，设置为温度最大值
                if value > self.MAX_SPEED:
                    value = self.MAX_SPEED
                    self.lineEdit_SpeedSet.setText(str(value))

                registers = self.convert_float_to_registers(value)
                self.modbusClient.write_registers(self.addrSpeedSet, registers, unit=1)

    def closeEvent(self, event):
        """窗体关闭事件"""
        reply = QMessageBox.question(self, '提示', "确定退出吗?",
                                     QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
        # 判断返回值，如果点击的是Yes按钮，我们就关闭组件和应用，否则就忽略关闭事件
        if reply == QMessageBox.Yes:
            if self.isConnect:
                self.disconnectModubs()
            event.accept()
        else:
            event.ignore()

    def convert_float_to_registers(self, value):
        """将浮点数转换为两个16位的寄存器"""
        # 大小端问题， 原数据为 0x1234 5678
        # Word Order - Big      Byte Order - Big       word1 =0x1234  word2 = 0x5678
        # Word Order - Big      Byte Order - Little    word1 =0x3412  word2 = 0x7856
        # Word Order - Little   Byte Order - Big       word1 = 0x5678 word2 = 0x1234
        # Word Order - Little   Byte Order - Little    word1 =0x7856 word2 = 0x3412
        builder = BinaryPayloadBuilder(wordorder=Endian.Big, byteorder=Endian.Big)
        builder.add_32bit_float(value)
        registers = builder.to_registers()
        return registers


if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = MainForm()
    demo.show()
    sys.exit(app.exec_())